package com.googlecode.archimedes.filegen.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

/**
 * <p>
 * Abstract super-class for Entities and Interfaces
 * </p>
 * <p>
 * Created: Jun 22, 2005
 * </p>
 * 
 * @author Erik Gustavson
 * @version $Id: AbstractType.java 2088 2006-03-16 01:56:24 +0000 (Thu, 16 Mar 2006) erik $
 */
public abstract class AbstractType extends AbstractAsset {

	private String				entityName			= "test";
	private String				displayName			= "TestDisplay";
	private String				extendsReference	= null;
	private String				_package			= null;

	protected Set<Member>		members				= new HashSet<Member>();
	protected Set<Relationship>	relationships		= new HashSet<Relationship>();
	private String				description			= "No Description.";
	public Set<AbstractMethod>	abstractMethods		= new HashSet<AbstractMethod>();

	/**
	 * Returns the AbstractType this AbstractType extends. Can be null if there is no parent type.
	 * 
	 * @return
	 */
	protected abstract AbstractType getExtendedType();

	/**
	 * if true, also include inherited members.
	 * 
	 * @param includeInherited
	 * @return
	 */
	public Set<Member> getMembers(boolean includeInherited) {

		List<Member> results = new ArrayList<Member>(members);

		if ((includeInherited) && (getExtendedType() != null)) {
			results.addAll(getExtendedType().getMembers(includeInherited));
		}

		Collections.sort(results);

		return new LinkedHashSet<Member>(results);
	}

	/**
	 * @see com.tps.filegen.model.AbstractType#getRelationships(boolean)
	 */
	public Set<Relationship> getRelationships(boolean includeInherited) {
		List<Relationship> results = new ArrayList<Relationship>(relationships);

		if ((includeInherited) && (getExtendedType() != null)) {
			results.addAll(getExtendedType().getRelationships(includeInherited));
		}

		Collections.sort(results);

		return new LinkedHashSet<Relationship>(results);
	}

	/**
	 * @return
	 */
	public String getExtendsReference() {
		return extendsReference;
	}

	public void setExtendsReference(String arg0) {
		this.extendsReference = arg0;
	}

	/**
	 * @return the EntityName with the first letter capitalized
	 */
	public String getEntityName() {
		return entityName;
	}

	/**
	 * @param entityName
	 *            automatically resets the string to cap firstletter, lowercase everything else
	 */
	public void setEntityName(String entityName) {
		this.entityName = entityName;
	}

	/**
	 * @return
	 */
	public Set<Member> getMembers() {
		return getMembers(true);
	}

	/**
	 * @param members
	 */
	public void setMembers(Set<Member> members) {
		this.members = members;
	}

	public void addMember(Member member) {
		members.add(member);
	}

	/**
	 * Loops through all relationships and pulls the entity that is NOT equal to the name of this Entity from any position in a relationship
	 * 
	 * @param
	 * @return a list of Entities TODO write a unit test
	 */
	public List<AbstractType> getAllRelatedEntities() {

		List<AbstractType> allList = new ArrayList<AbstractType>();
		allList.addAll(this.getInboundRelatedEntities());
		allList.addAll(this.getOutboundRelatedEntities());
		return allList;
	}

	/**
	 * Returns a list of relationships where this entity is the source entity.
	 * 
	 * @return
	 */
	public List<Relationship> getSourceRelationships() {
		return getRelationships(End.Source);
	}

	/**
	 * Returns a list of relationships where this entity is the target entity.
	 * 
	 * @return
	 */
	public List<Relationship> getTargetRelationships() {
		return getRelationships(End.Target);
	}

	/**
	 * Returns a list of relationships where this entity is either on the Source or Target end of the relationship.
	 * 
	 * @param isSource
	 *            true if this entity is the Source. False if this entity should be the target
	 * @return a List<Relationship> of relationships
	 */
	protected List<Relationship> getRelationships(End whichSide) {
		List<Relationship> results = new ArrayList<Relationship>();

		for (Relationship rel : getRelationships()) {

			AbstractType entityToTest = (whichSide == End.Source) ? rel.getFromEntity() : rel.getToEntity();
			try {
				// if the entityname to eval matches the current entity, we are good to go
				if (getEntityName().equals(entityToTest.getEntityName())) {
					results.add(rel);

				} else if (matchesAParent(entityToTest)) {
					results.add(rel);

				} else if ((entityToTest instanceof Interface) && (this instanceof Entity)) {
					// perform an additional check - if the entityToTest is really and interface, see if the current entity
					// implements that interface or not. If it does, we want to pass through the relationship as if it were
					// our own.
					if (((Entity) this).getInterfaces().contains(entityToTest)) {
						results.add(rel);
					}
				}
			} catch (Exception e) {
				log.info("FROM Entity: " + rel.getFromEntity());
				log.info("TO Entity: " + rel.getToEntity());
				log.info("Rel Type: " + rel.getClass());
				throw new RuntimeException("AbstractType [" + entityToTest + "] has problems on Relationship [" + rel + "] for direction [" + whichSide + "].", e);
			}
		}

		return results;
	}

	/**
	 * Recursivly search up the extension tree for a matching entity. Return true if one is found, otherwise return false.
	 * 
	 * @param entityToTest
	 * @return
	 */
	public boolean matchesAParent(AbstractType entityToTest) {
		if (this.getExtendedType() != null) {
			if (getExtendedType().getEntityName().equals(entityToTest.getEntityName())) {
				return true;
			}

			return getExtendedType().matchesAParent(entityToTest);
		}

		return false;
	}

	/**
	 * Loops through all relationships and find those where <b>this</b> entity is on the <b>target</b> end of the relationship. In other words, where
	 * the relationship is <b>inbound</b> from this entity. For example, if "Customers have Orders", the looking for source entities on Order would
	 * return Customer.
	 * 
	 * @return a list of Entities where this entity is the target.
	 */
	public Set<AbstractType> getInboundRelatedEntities() {
		return this.getRelatedEntities(End.Target);
	}

	/**
	 * Loops through all relationships and find those where <b>this</b> entity is on the <b>source</b> end of the relationship. In other words, where
	 * the relationship is <b>outbound</b> from this entity. For example, if "Customers have Orders", the looking for source entities on Customer
	 * would return Order.
	 * 
	 * @return a list of Entities where this entity is the target.
	 */
	public Set<AbstractType> getOutboundRelatedEntities() {
		return this.getRelatedEntities(End.Source);
	}

	/**
	 * Loops through all relationships and pulls out the Entities that are related to this entity on the specified side of the reltationship.
	 * 
	 * @param isSource
	 *            true if you want Entities where this entity is the source. False is this entity is the target.
	 */
	private Set<AbstractType> getRelatedEntities(End isSource) {
		// flip the isSource boolean because we want relationships where we are on one
		// side but then we want to pull the entity from the other side of the relationship.
		// If we did not flip the isSource boolean, we would always get back outselves.

		End end = (isSource == End.Source) ? End.Target : End.Source;

		return getEntitiesOutOfRelationships(getRelationships(isSource), end);
	}

	/**
	 * Returns true if this entity has a parent class.
	 * 
	 * @return
	 */
	public boolean isChild() {
		return !getParentRelationships().isEmpty();
	}

	/**
	 * Returns a list of Entities where this entity is the parent entity.
	 * 
	 * @return true if the entityHas a Parent
	 */
	public Set<AbstractType> getParentEntities() {
		return getEntitiesOutOfRelationships(getParentRelationships(), End.Source);
	}

	/**
	 * Returns a list of Relationships where this entity is the source of the relationship.
	 * 
	 * @return true if the entityHas a Parent
	 */
	private List<Relationship> getParentRelationships() {

		List<Relationship> relationships = getRelationships(End.Target);
		Iterator<Relationship> iter = relationships.iterator();

		while (iter.hasNext()) {
			Relationship rel = iter.next();
			// technically, the LHS of a M:M is not a parent
			if (rel.getAssociation().equals(Association.ManyToMany)) {
				iter.remove();
			}
			if (rel.getAssociation().equals(Association.ManyToOne)) {
				iter.remove();
			}
		}

		return relationships;
	}

	/**
	 * Utility method to Entities out of a List of relationships.
	 * 
	 * @param relationships
	 *            The List of relationships to examine
	 * @param isSource
	 *            true if looking for entities on the source side, false if looking for entities on the target side
	 * @return a List of Entities
	 */
	private Set<AbstractType> getEntitiesOutOfRelationships(List<Relationship> relationships, End isSource) {
		Set<AbstractType> results = new HashSet<AbstractType>();

		for (Relationship rel : relationships) {
			AbstractType entity = (isSource == End.Source) ? rel.getFromEntity() : rel.getToEntity();
			results.add(entity);
		}

		return results;
	}

	/**
	 * @return
	 */
	public Set<Relationship> getRelationships() {
		return getRelationships(true);
	}

	/**
	 * @param set
	 *            the list of relationships
	 */
	public void setRelationship(Set<Relationship> relationships) {
		this.relationships = relationships;
	}

	public void addRelationship(Relationship relationship) {
		relationships.add(relationship);
	}

	/**
	 * @return
	 */
	public String getDisplayName() {
		return displayName;
	}

	/**
	 * @param displayName
	 */
	public void setDisplayName(String displayName) {
		this.displayName = displayName;
	}

	/**
	 * Print the Entity out as a String
	 * 
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		return entityName;
	}

	/**
	 * Returns the collection of members contained in this Entity.
	 * 
	 * @see com.tps.filegen.model.Asset#getSubordinateAssets()
	 */
	public Collection<? extends Asset> getSubordinateAssets() {
		return this.getMembers();
	}

	/**
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj instanceof AbstractType == false) {
			return false;
		}
		if (this == obj) {
			return true;
		}

		AbstractType rhs = (AbstractType) obj;
		return new EqualsBuilder().append(entityName, rhs.entityName).isEquals();
	}

	/**
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		// you pick a hard-coded, randomly chosen, non-zero, odd number
		// ideally different for each class
		return new HashCodeBuilder(19, 29).append(entityName).toHashCode();
	}

	/**
	 * Returns the description.
	 * 
	 * @return String value of description
	 */
	public String getDescription() {
		return description;
	}

	/**
	 * Sets the description
	 * 
	 * @param description
	 *            The description to set.
	 */
	public void setDescription(String description) {
		this.description = description;
	}

	/**
	 * Returns the abstractMethods.
	 * 
	 * @return Set<AbstractMethod> value of abstractMethods
	 */
	public Set<AbstractMethod> getAbstractMethods() {
		return abstractMethods;
	}

	/**
	 * Sets the abstractMethods
	 * 
	 * @param abstractMethods
	 *            The abstractMethods to set.
	 */
	public void setAbstractMethods(Set<AbstractMethod> abstractMethods) {
		this.abstractMethods = abstractMethods;
	}

	/**
	 * Sets the abstractMethods
	 * 
	 * @param abstractMethods
	 *            The abstractMethods to set.
	 */
	public void addAbstractMethod(AbstractMethod abstractMethod) {
		getAbstractMethods().add(abstractMethod);
	}

	/**
	 * @param _package
	 *            the _package to set
	 */
	public void setPackage(String _package) {
		this._package = _package;
	}

	/**
	 * @return the _package
	 */
	public String getPackage() {
		return _package;
	}
}
